{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9d5472d",
   "metadata": {},
   "outputs": [],
   "source": [
    "SCOPETYPE = 'OPENADC'\n",
    "PLATFORM = 'CW305_100t'\n",
    "fpga_id = '100t'\n",
    "bitstream = r\"G:\\RISCV_MLDSA\\top_cw305_32MHZ.bit\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "70fc8450",
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer as cw\n",
    "\n",
    "try:\n",
    "    if not scope.connectStatus:\n",
    "        scope.con()\n",
    "except NameError:\n",
    "    scope = cw.scope()\n",
    "\n",
    "try:\n",
    "    if SS_VER == \"SS_VER_2_1\":\n",
    "        target_type = cw.targets.SimpleSerial2\n",
    "    elif SS_VER == \"SS_VER_2_0\":\n",
    "        raise OSError(\"SS_VER_2_0 is deprecated. Use SS_VER_2_1\")\n",
    "    else:\n",
    "        target_type = cw.targets.SimpleSerial\n",
    "except:\n",
    "    SS_VER=\"SS_VER_1_1\"\n",
    "    target_type = cw.targets.SimpleSerial\n",
    "\n",
    "try:\n",
    "    target = cw.target(scope, target_type)\n",
    "except:\n",
    "    print(\"INFO: Caught exception on reconnecting to target - attempting to reconnect to scope first.\")\n",
    "    print(\"INFO: This is a work-around when USB has died without Python knowing. Ignore errors above this line.\")\n",
    "    scope = cw.scope()\n",
    "    target = cw.target(scope, target_type)\n",
    "\n",
    "\n",
    "print(\"INFO: Found ChipWhisperer😍\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa5380b8",
   "metadata": {},
   "outputs": [],
   "source": [
    "if \"STM\" in PLATFORM or PLATFORM == \"CWLITEARM\" or PLATFORM == \"CWNANO\":\n",
    "    prog = cw.programmers.STM32FProgrammer\n",
    "elif PLATFORM == \"CW303\" or PLATFORM == \"CWLITEXMEGA\":\n",
    "    prog = cw.programmers.XMEGAProgrammer\n",
    "elif \"neorv32\" in PLATFORM.lower():\n",
    "    prog = cw.programmers.NEORV32Programmer\n",
    "elif PLATFORM == \"CW308_SAM4S\":\n",
    "    prog = cw.programmers.SAM4SProgrammer\n",
    "else:\n",
    "    prog = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5bc96921",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "time.sleep(0.05)\n",
    "scope.default_setup()\n",
    "\n",
    "if PLATFORM == \"CW308_SAM4S\" or PLATFORM == \"CWHUSKY\":\n",
    "    scope.io.target_pwr = 0\n",
    "    time.sleep(0.2)\n",
    "    scope.io.target_pwr = 1\n",
    "    time.sleep(0.2)\n",
    "def reset_target(scope):\n",
    "    if PLATFORM == \"CW303\" or PLATFORM == \"CWLITEXMEGA\":\n",
    "        scope.io.pdic = 'low'\n",
    "        time.sleep(0.01)\n",
    "        scope.io.pdic = 'high_z' #XMEGA doesn't like pdic driven high\n",
    "        time.sleep(0.01) #xmega needs more startup time\n",
    "    elif \"neorv32\" in PLATFORM.lower():\n",
    "        raise IOError(\"Default iCE40 neorv32 build does not have external reset - reprogram device to reset\")\n",
    "    elif PLATFORM == \"CW308_SAM4S\":\n",
    "        scope.io.nrst = 'low'\n",
    "        time.sleep(0.0125)\n",
    "        scope.io.nrst = 'high_z'\n",
    "        time.sleep(0.0125)\n",
    "    else:  \n",
    "        scope.io.nrst = 'low'\n",
    "        time.sleep(0.005)\n",
    "        scope.io.nrst = 'high_z'\n",
    "        time.sleep(0.005)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80a91103",
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer as cw\n",
    "import serial\n",
    "import binascii\n",
    "import time\n",
    "target = cw.target(scope=scope, \n",
    "                   target_type=cw.targets.CW305, \n",
    "                   fpga_id='100t',\n",
    "                   bsfile=bitstream,\n",
    "                   force=True)\n",
    "\n",
    "\n",
    "\n",
    "target.vccint_set(1.0)\n",
    "# we only need PLL1:\n",
    "target.pll.pll_enable_set(True)\n",
    "target.pll.pll_outenable_set(False, 0)\n",
    "target.pll.pll_outenable_set(True, 1)\n",
    "target.pll.pll_outenable_set(False, 2)\n",
    "target.pll.pll_outfreq_set(32E6, 1)\n",
    "target.clkusbautooff = True\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "134aac0e",
   "metadata": {},
   "outputs": [],
   "source": [
    "portx = \"COM3\"\n",
    "bps = 115200\n",
    "\n",
    "\n",
    "# 打开串口\n",
    "ser = serial.Serial(portx, bps, timeout=None)\n",
    "ser.flushInput()  # 清空输入缓冲区\n",
    "\n",
    "def ln_wr( ser, x ) :\n",
    "    ser.write((x + '\\x0D').encode('ascii'))\n",
    "    ser.flush()\n",
    "# time.sleep( 0.1 )\n",
    "\n",
    "def ln_rd( ser    ) :\n",
    "    r = ''\n",
    "    while( True ):\n",
    "        t = ser.read( 1 ).decode( 'ascii' )\n",
    "        if( t == '\\x0D' ):\n",
    "            break\n",
    "        else:\n",
    "            r += t\n",
    "    # time.sleep( 0.1 )\n",
    "    return r\n",
    "def bytes2octetstr( x ) :\n",
    "    n = '{0:02X}'.format( len( x ) )\n",
    "    x = binascii.b2a_hex( x ).decode( 'ascii' ).upper()\n",
    "    return n + ':' + x\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "032d9605",
   "metadata": {},
   "outputs": [],
   "source": [
    "import ctypes\n",
    "import numpy as np\n",
    "from picosdk.ps5000a import ps5000a as ps\n",
    "import matplotlib.pyplot as plt\n",
    "from picosdk.functions import adc2mV, assert_pico_ok, mV2adc\n",
    "\n",
    "# Create chandle and status ready for use\n",
    "chandle = ctypes.c_int16()\n",
    "status = {}\n",
    "\n",
    "# Open 5000 series PicoScope\n",
    "# Resolution set to 12 Bit\n",
    "# 设置示波器的BIT，默认为12BIT\n",
    "resolution =ps.PS5000A_DEVICE_RESOLUTION[\"PS5000A_DR_12BIT\"]\n",
    "# Returns handle to chandle for use in future API functions\n",
    "status[\"openunit\"] = ps.ps5000aOpenUnit(ctypes.byref(chandle), None, resolution)\n",
    "\n",
    "try:\n",
    "    assert_pico_ok(status[\"openunit\"])\n",
    "except: # PicoNotOkError:\n",
    "    powerStatus = status[\"openunit\"]\n",
    "    if powerStatus == 286:\n",
    "        status[\"changePowerSource\"] = ps.ps5000aChangePowerSource(chandle, powerStatus)\n",
    "    elif powerStatus == 282:\n",
    "        status[\"changePowerSource\"] = ps.ps5000aChangePowerSource(chandle, powerStatus)\n",
    "    else:\n",
    "        raise\n",
    "    assert_pico_ok(status[\"changePowerSource\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "79f7f6a3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set up channel A\n",
    "# handle = chandle\n",
    "\n",
    "# 注意这里要改成直流电！\n",
    "channel = ps.PS5000A_CHANNEL[\"PS5000A_CHANNEL_A\"]\n",
    "# enabled = 1\n",
    "coupling_type = ps.PS5000A_COUPLING[\"PS5000A_DC\"]\n",
    "# 更改A通道（采集触发信号的通道）的范围\n",
    "chARange = ps.PS5000A_RANGE[\"PS5000A_20V\"]\n",
    "# analogue offset = 0 V\n",
    "status[\"setChA\"] = ps.ps5000aSetChannel(chandle, channel, 1, coupling_type, chARange, 0)\n",
    "assert_pico_ok(status[\"setChA\"])\n",
    "\n",
    "# Set up channel B\n",
    "# handle = chandle\n",
    "channel = ps.PS5000A_CHANNEL[\"PS5000A_CHANNEL_B\"]\n",
    "# enabled = 1\n",
    "\n",
    "# 注意这里要改成交流电！!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n",
    "coupling_type2 = ps.PS5000A_COUPLING[\"PS5000A_AC\"]\n",
    "#chBRange = ps.PS5000A_RANGE[\"PS5000A_2V\"]\n",
    "# 更改B通道（采出来数据的通道）的范围\n",
    "chBRange = ps.PS5000A_RANGE[\"PS5000A_200MV\"]\n",
    "# analogue offset = 0 V\n",
    "status[\"setChB\"] = ps.ps5000aSetChannel(chandle, channel, 1, coupling_type2, chBRange, 0)\n",
    "assert_pico_ok(status[\"setChB\"])\n",
    "\n",
    "# find maximum ADC count value\n",
    "# handle = chandle\n",
    "# pointer to value = ctypes.byref(maxADC)\n",
    "maxADC = ctypes.c_int16()\n",
    "status[\"maximumValue\"] = ps.ps5000aMaximumValue(chandle, ctypes.byref(maxADC))\n",
    "assert_pico_ok(status[\"maximumValue\"])\n",
    "\n",
    "# Set up single trigger\n",
    "# handle = chandle\n",
    "# enabled = 1\n",
    "source = ps.PS5000A_CHANNEL[\"PS5000A_CHANNEL_A\"]\n",
    "threshold = int(mV2adc(2500,chARange, maxADC))\n",
    "# direction = PS5000A_RISING = 2\n",
    "# delay = 0 s\n",
    "# auto Trigger = 1000 ms\n",
    "status[\"trigger\"] = ps.ps5000aSetSimpleTrigger(chandle, 1, source, threshold, 2, 0, 1000)\n",
    "assert_pico_ok(status[\"trigger\"])\n",
    "\n",
    "# Set number of pre and post trigger samples to be collected\n",
    "#####################参数###############################################\n",
    "# 在触发之前采集的数量\n",
    "preTriggerSamples = 0\n",
    "# 在触发之后采集的数量\n",
    "# postTriggerSamples = 62600\n",
    "postTriggerSamples = 78000 # online timebase = 8\n",
    "# postTriggerSamples = 46000 # single mul timebase = 3\n",
    "\n",
    "#####################################################################\n",
    "maxSamples = preTriggerSamples + postTriggerSamples\n",
    "\n",
    "# Get timebase information\n",
    "# Warning: When using this example it may not be possible to access all Timebases as all channels are enabled by default when opening the scope.  \n",
    "# To access these Timebases, set any unused analogue channels to off.\n",
    "# handle = chandle\n",
    "\n",
    "#####################参数#############################\n",
    "#timebase = 8\n",
    "# 通过timebase来控制采样率\n",
    "# 采样率的计算可以查手册，也可以用pico3000的那个控件来查看\n",
    "timebase = 5\n",
    "######################################################\n",
    "#timebase = 8  , 20.8 MHZ\n",
    "# noSamples = maxSamples\n",
    "# pointer to timeIntervalNanoseconds = ctypes.byref(timeIntervalns)\n",
    "# pointer to maxSamples = ctypes.byref(returnedMaxSamples)\n",
    "# segment index = 0\n",
    "timeIntervalns = ctypes.c_float()\n",
    "returnedMaxSamples = ctypes.c_int32()\n",
    "status[\"getTimebase2\"] = ps.ps5000aGetTimebase2(chandle, timebase, maxSamples, ctypes.byref(timeIntervalns), ctypes.byref(returnedMaxSamples), 0)\n",
    "assert_pico_ok(status[\"getTimebase2\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24786d2b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 获取采样间隔\n",
    "timeInterval_ns = timeIntervalns.value\n",
    "\n",
    "# 计算采样率\n",
    "sampling_interval_seconds = timeInterval_ns * 1e-9\n",
    "sampling_rate_hz = 1.0 / sampling_interval_seconds\n",
    "\n",
    "print(f\"Sampling interval: {timeInterval_ns} ns\")\n",
    "print(f\"Sampling rate: {sampling_rate_hz / 1e6:.2f} MS/s\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c59cc4ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib notebook\n",
    "import matplotlib.pylab as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7b40c9e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "43feb0c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def convert_msg(m,length):\n",
    "    res = []\n",
    "    for j in range(0,length): \n",
    "        s = m[j*2:j*2+2]\n",
    "        s = \"0x\" + s\n",
    "        res.append(int(s,16))\n",
    "        #print(s,end=',')\n",
    "    \n",
    "    return res\n",
    "\n",
    "def little_to_big_endian(hex_str):\n",
    "    # 每2个字符为一组，反转顺序\n",
    "    bytes_list = [hex_str[i:i+2] for i in range(0, len(hex_str), 2)]\n",
    "    bytes_list.reverse()\n",
    "    return ''.join(bytes_list).upper()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71ebfe9e",
   "metadata": {},
   "source": [
    "下面是攻击集部分"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "771e22aa",
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "from tqdm import tnrange\n",
    "from tqdm.auto import tqdm\n",
    "import gc\n",
    "\n",
    "MLEN = 33\n",
    "SEEDBYTES = 32\n",
    "rhoprime_len = 48\n",
    "\n",
    "L = 4\n",
    "\n",
    "\n",
    "\n",
    "def mainProcess(num_of_traces, myfile):\n",
    "\n",
    "    traces_array = np.empty(shape=(traces_per_part,maxSamples),dtype=np.float32)\n",
    "    trig_array = np.empty(shape=(traces_per_part,maxSamples),dtype=np.float32)\n",
    "\n",
    "    \n",
    "\n",
    "    for index_i in tnrange(num_of_traces):\n",
    "        ser.flushInput()  # 清空缓冲区\n",
    "        reset_target(scope)\n",
    "        global global_counter\n",
    "        global pbar\n",
    "        msg_input = []\n",
    "\n",
    "        line = infile.readline()\n",
    "        while(line.find('counter=') == -1):\n",
    "            line = infile.readline()\n",
    "            \n",
    "        counter  = line\n",
    "\n",
    "        line = infile.readline()\n",
    "        line = infile.readline()\n",
    "        \n",
    "        index_rhoprime = line.find('rhoprime=') + 9\n",
    "        \n",
    "\n",
    "        msg_input += convert_msg(line[index_rhoprime:index_rhoprime+rhoprime_len*2], rhoprime_len)\n",
    "        ln_wr(ser,bytes2octetstr(bytes(msg_input)))\n",
    "        \n",
    "        line = infile.readline()\n",
    "\n",
    "        \n",
    "        nonce_index = line.find('nonce=') + 6\n",
    "        nonce = int(line[nonce_index:nonce_index+4],16)\n",
    "        nonce = (nonce - 1) * L\n",
    "#         print(line,nonce)\n",
    "        line = infile.readline()\n",
    "        mark_index_st = line.find('mark_index=') + 11\n",
    "        mark_index = int(line[mark_index_st:mark_index_st+1])\n",
    "\n",
    "        \n",
    "        nonce += int(mark_index)\n",
    "        \n",
    "        #nonce 是uint16 需小端序输入\n",
    "        nonce_bytes = nonce.to_bytes(2, 'big')\n",
    "#         print(nonce_bytes)\n",
    "        ln_wr(ser,bytes2octetstr(nonce_bytes))\n",
    "#         msg_input += [int(nonce_bytes[0])]\n",
    "#         msg_input += [int(nonce_bytes[1])]\n",
    "\n",
    "\n",
    "        \n",
    "        line = infile.readline()\n",
    "        \n",
    "        r_f4bytes_st = line.find('r_f4bytes') + 13\n",
    "        r_f4bytes = line[r_f4bytes_st:-1]\n",
    "        s = ''\n",
    "        for x in range(4):\n",
    "            piece = r_f4bytes[x*8:x*8+8]\n",
    "            ss = little_to_big_endian(piece)\n",
    "            s+=ss\n",
    "            s+=' '\n",
    "#         print(\"chk:\",s)\n",
    "        r_f4bytes = list(convert_msg(r_f4bytes,16))\n",
    "        \n",
    "        line = infile.readline()\n",
    "        r_l4bytes_st = line.find('r_l4bytes') + 13\n",
    "        r_l4bytes = line[r_l4bytes_st:-1]\n",
    "        r_l4bytes = list(convert_msg(r_l4bytes,16))\n",
    "\n",
    "        \n",
    "        global_counter += 1\n",
    "\n",
    "        status[\"runBlock\"] = ps.ps5000aRunBlock(chandle, preTriggerSamples, postTriggerSamples, timebase, None, 0, None, None)\n",
    "        assert_pico_ok(status[\"runBlock\"])\n",
    "        \n",
    "        ready = ctypes.c_int16(0)\n",
    "        check = ctypes.c_int16(0)\n",
    "        while ready.value == check.value:\n",
    "            status[\"isReady\"] = ps.ps5000aIsReady(chandle, ctypes.byref(ready))\n",
    "        bufferAMax = (ctypes.c_int16 * maxSamples)()\n",
    "        bufferAMin = (ctypes.c_int16 * maxSamples)() \n",
    "        bufferBMax = (ctypes.c_int16 * maxSamples)()\n",
    "        bufferBMin = (ctypes.c_int16 * maxSamples)()\n",
    "        source = ps.PS5000A_CHANNEL[\"PS5000A_CHANNEL_A\"]\n",
    "        status[\"setDataBuffersA\"] = ps.ps5000aSetDataBuffers(chandle, source, ctypes.byref(bufferAMax), ctypes.byref(bufferAMin), maxSamples, 0, 0)\n",
    "        assert_pico_ok(status[\"setDataBuffersA\"])\n",
    "        source = ps.PS5000A_CHANNEL[\"PS5000A_CHANNEL_B\"]\n",
    "        status[\"setDataBuffersB\"] = ps.ps5000aSetDataBuffers(chandle, source, ctypes.byref(bufferBMax), ctypes.byref(bufferBMin), maxSamples, 0, 0)\n",
    "        assert_pico_ok(status[\"setDataBuffersB\"])\n",
    "        overflow = ctypes.c_int16()\n",
    "        cmaxSamples = ctypes.c_int32(maxSamples)\n",
    "        \n",
    "        \n",
    "        status[\"getValues\"] = ps.ps5000aGetValues(chandle, 0, ctypes.byref(cmaxSamples), 0, 0, 0, ctypes.byref(overflow))\n",
    "        assert_pico_ok(status[\"getValues\"])\n",
    "        adc2mVChAMax =  adc2mV(bufferAMax, chARange, maxADC)\n",
    "        adc2mVChBMax =  adc2mV(bufferBMax, chBRange, maxADC)\n",
    "        # ❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n",
    "        # 正式采集之前，先采集一条曲线的触发信号，并画图观察触发的上升沿和下降沿是否全部捕捉到，若看不到下降沿，则尝试增大postTriggerSamples\n",
    "#         plt.plot(adc2mVChAMax[:])\t# 画图观察触发信号是否正常\n",
    "#         plt.plot(adc2mVChBMax[:])\t# 画图观察数据信号\n",
    "        if adc2mVChBMax[0] == 32512 and adc2mVChBMax[-1] == 32512:\n",
    "            raise Exception('示波器采集错误，请重新采集')\n",
    "            \n",
    "        time.sleep(0.01)\n",
    "        \n",
    "        r=ln_rd(ser)\n",
    "#         print(s)\n",
    "#         print(r)\n",
    "        if s!=r:\n",
    "            print(\"chk:\",s)\n",
    "            print(\"ret:\",r)\n",
    "        r=0\n",
    "        traces_array[index_i] = np.array(adc2mVChBMax[:])\n",
    "        trig_array[index_i] = np.array(adc2mVChAMax[:])\n",
    "        ser.flushOutput()\n",
    "\n",
    "                \n",
    "        \n",
    "        \n",
    "        global_counter += 1\n",
    "        pbar.update(1)\n",
    "    return traces_array, trig_array\n",
    "\n",
    "\n",
    "\n",
    "for i in range(1):\n",
    "    ################这里是参数部分，可以修改##############################\n",
    "    traces_per_part = 500  # 每一块文件多少条曲线\n",
    "    part = 8 # 分成多少块文件\n",
    "\n",
    "    part_start_index = 0  # 序号从多少块开始。如果是初始采集，就设置为0\n",
    "\n",
    "    #################################################################\n",
    "    filename_of_traces=r\"D:\\MLDSA_RISCV\\traces\".format(i)\n",
    "    infile=open(r\"G:\\RISCV_MLDSA\\seed_attack.txt\")\n",
    "    # seed url\n",
    "    global_counter = 0\n",
    "    total_traces = part * traces_per_part  # 总共要采集的曲线数目\n",
    "    pbar = tqdm(total=total_traces)\n",
    "    # 先找到和part_start_index对应的counter\n",
    "    while(global_counter != traces_per_part * part_start_index):\n",
    "        line = infile.readline()   \n",
    "        if(line.find('counter=')!= -1):\n",
    "            global_counter += 1\n",
    "    print(\"here\",global_counter )\n",
    "    for p in range(part):  # 遍历所有的part\n",
    "        traces_arr, trig_arr = mainProcess(traces_per_part, infile)\n",
    "        np.save(filename_of_traces + \"_part{0}.npy\".format(p), traces_arr)\n",
    "        np.save(filename_of_traces + \"_trig.npy\".format(p), trig_arr)\n",
    "\n",
    "        del traces_arr\n",
    "        del trig_arr\n",
    "      \n",
    "        gc.collect()\n",
    "    pbar.close()\n",
    "    infile.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6ef3e22d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Stop the scope\n",
    "# handle = chandle\n",
    "status[\"stop\"] = ps.ps5000aStop(chandle)\n",
    "assert_pico_ok(status[\"stop\"])\n",
    "\n",
    "# Close unit Disconnect the scope\n",
    "# handle = chandle\n",
    "status[\"close\"]=ps.ps5000aCloseUnit(chandle)\n",
    "assert_pico_ok(status[\"close\"])\n",
    "\n",
    "# display status returns\n",
    "print(status)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
